﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;

namespace Heimdallr.Net
{
    /// <summary>
    /// Represents the HTTP server.
    /// </summary>
    /// <remarks>
    /// Adding modules to <see cref="Modules"/> property for handling incoming request and outgoing response.
    /// Each module corresponds to a combination of URL path and HTTP method.
    /// For handling requests, a module must implement <see cref="IHttpRequestModule"/> interface.
    /// For handling responses, implements <see cref="IHttpResponseModule"/> interface.
    /// </remarks>
    public class HttpElement : ServiceElement, INotifyElement, ITriggerElement
    {
        #region Field

        internal readonly static Uri[] EmptyPrefixes = new Uri[] { };

        private readonly HttpModuleCollection modules;
        private readonly UriBuilder prefixUri;
        private readonly ConcurrentDictionary<string, IEnumerable<IHttpRequestModule>> requestMapping;
        private readonly ConcurrentDictionary<string, IEnumerable<IHttpResponseModule>> responseMapping;

        private HttpListener listener;
        private int maxReceivedSize;
        private string realm;

        #endregion

        #region Property

        int ITriggerElement.MaxReceivedSize
        {
            get { return this.maxReceivedSize; }
            set { this.maxReceivedSize = value; }
        }

        /// <summary>
        /// Gets or sets the authentication realm for digest or basic authentication.
        /// </summary>
        /// <exception cref="InvalidOperationException">This property cannot be changed once the element has started.</exception>
        public string Realm
        {
            get { return this.realm; }
            set
            {
                if (base.Status != ServiceElementStatus.Stopped)
                    throw new InvalidOperationException("This property cannot be changed once the service has started.");

                this.realm = value;
            }
        }

        /// <summary>
        /// Gets the hostname of current instance.
        /// </summary>
        /// <exception cref="InvalidOperationException">This property cannot be changed once the element has started.</exception>
        public string Host
        {
            get { return this.prefixUri.Host; }
            set
            {
                if (base.Status != ServiceElementStatus.Stopped)
                    throw new InvalidOperationException("This property cannot be changed once the service has started.");

                this.prefixUri.Host = value;
            }
        }

        /// <summary>
        /// Gets or sets the port of current HTTP server.
        /// </summary>
        /// <exception cref="InvalidOperationException">This property cannot be changed once the element has started.</exception>
        public int Port
        {
            get { return this.prefixUri.Port; }
            set
            {
                if (base.Status != ServiceElementStatus.Stopped)
                    throw new InvalidOperationException("This property cannot be changed once the service has started.");

                this.prefixUri.Port = value;
            }
        }

        /// <summary>
        /// Gets the collection of modules.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Each item of this collection can not be null.
        /// And the combination of <see cref="HttpModule.RelativePath"/> and <see cref="HttpModule.Method"/> must be unique.
        /// </para>
        /// <para>
        /// Adding modules to <see cref="Modules"/> property for handling incoming request and outgoing response.
        /// Each module corresponds to a combination of URL path and HTTP method.
        /// For handling requests, a module must implement <see cref="IHttpRequestModule"/> interface.
        /// For handling responses, implements <see cref="IHttpResponseModule"/> interface.
        /// </para>
        /// </remarks>
        public IList<HttpModule> Modules
        {
            get { return this.modules; }
        }

        /// <summary>
        /// Gets all modules for handling requests.
        /// </summary>
        public IEnumerable<IHttpRequestModule> RequestModules
        {
            get { return HttpModuleCollection.FindModules<IHttpRequestModule, IHttpRequestModule>(this.modules); }
        }

        /// <summary>
        /// Gets all modules for handling responses.
        /// </summary>
        public IEnumerable<IHttpResponseModule> ResponseModules
        {
            get { return HttpModuleCollection.FindModules<IHttpResponseModule, IHttpResponseModule>(this.modules); }
        }

        /// <summary>
        /// Gets all listening prefix URIs of this instance. 
        /// </summary>
        public IEnumerable<Uri> ListeningPrefixes
        {
            get
            {
                if (this.listener == null)
                    return EmptyPrefixes;
                else
                    return this.listener.Prefixes.Select(p => new Uri(p, UriKind.Absolute)).ToArray();
            }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when a notification has been created.
        /// </summary> 
        public event FireNotificationEventHandler Notify;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        public HttpElement()
            : this(Dns.GetHostName(), 80, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of this class, using specified host name.
        /// </summary>
        /// <param name="host">Specified host name.</param>
        /// <exception cref="ArgumentNullException"><paramref name="host"/> is null.</exception>
        public HttpElement(string host)
            : this(host, 80, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of this class, using specified host name and port number.
        /// </summary>
        /// <param name="host">Specified host name.</param>
        /// <param name="portNumber">Specified port number.</param>
        /// <exception cref="ArgumentNullException"><paramref name="host"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="portNumber"/> is less than -1 or greater than 65,535.</exception>
        public HttpElement(string host, int portNumber)
            : this(host, portNumber, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of this class, using specified host name and port number.
        /// </summary>
        /// <param name="host">Specified host name.</param>
        /// <param name="portNumber">Specified port number.</param>
        /// <param name="isHttps">Indecates using HTTPS or not.</param>
        /// <exception cref="ArgumentNullException"><paramref name="host"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="portNumber"/> is less than -1 or greater than 65,535.</exception>
        public HttpElement(string host, int portNumber, bool isHttps)
        {
            if (host == null)
                throw new ArgumentNullException("host", "Value is null.");

            this.modules = new HttpModuleCollection();
            this.prefixUri = new UriBuilder(isHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, host, portNumber);
            this.requestMapping = new ConcurrentDictionary<string, IEnumerable<IHttpRequestModule>>();
            this.responseMapping = new ConcurrentDictionary<string, IEnumerable<IHttpResponseModule>>();

            base.CanPauseAndContinue = false;
        }

        #endregion

        #region INotifyElement Member

        /// <summary>
        /// Publishes a notification to all <see cref="ResponseModules"/>.
        /// </summary>
        /// <param name="notification">A notification which is going to be published.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="notification"/> is null.</exception>
        /// <exception cref="InvalidOperationException">This element has not been started.</exception>
        public void Publish(Notification notification)
        {
            if (notification == null)
                throw new ArgumentNullException("notification");

            if (base.Status != ServiceElementStatus.Running)
                throw new InvalidOperationException("This element has not been started.");

            foreach (var responseModule in this.ResponseModules)
                responseModule.Publish(notification);
        }

        #endregion

        #region ServiceElement Implementation Member

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Stopped</see> to <see cref="ServiceElementStatus">Running</see>.
        /// </summary>
        /// <exception cref="InvalidOperationException">Two or more of the combinations of relative path and HTTP method conflict.</exception>
        /// <exception cref="Exception">Can not start listening operation.</exception>
        protected sealed override void OnStart()
        {
            var pathAndMethodList = this.modules.SelectMany(PathAndMethod.FromModule);

            if (pathAndMethodList.Count() > pathAndMethodList.Distinct().Count())
                throw new InvalidOperationException("Two or more of the combinations of relative path and HTTP method conflict.");

            HttpListener lstnr = new HttpListener();

            foreach (PathAndMethod pathAndMethod in pathAndMethodList)
            {
                this.prefixUri.Path = pathAndMethod.RelativePath;
                lstnr.Prefixes.Add(this.prefixUri.Uri.ToString().ToLowerInvariant());
            }

            lstnr.AuthenticationSchemeSelectorDelegate = this.SelectScheme;
            lstnr.Realm = this.realm;

            try
            {
                lstnr.Start();
                lstnr.BeginGetContext(this.GetContextCallback, null);
            }
            catch (Exception error)
            {
                throw new Exception("Can not start listening operation.", error);
            }

            Interlocked.Exchange(ref this.listener, lstnr);
        }

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Running</see> to <see cref="ServiceElementStatus">Stopped</see>.
        /// </summary>
        protected sealed override void OnStop()
        {
            HttpListener lstnr = Interlocked.Exchange(ref this.listener, null);

            if (lstnr == null)
                return;

            try
            {
                lstnr.Prefixes.Clear();
                lstnr.Stop();
            }
#if DEBUG
            catch (Exception error)
            {
                Debug.WriteLine(error);
#else   
            catch
            {
#endif
            }
            finally
            {
                lstnr.Close();
            }
        }

        #endregion

        #region Method

        /// <summary>
        /// Gets allowed HTTP methods of a relative path.
        /// </summary>
        /// <param name="relativePath">A relative path.</param>
        /// <returns>The allowed HTTP methods of a relative path.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="relativePath"/> is null.</exception>
        public IEnumerable<string> GetAllowedHttpMethods(string relativePath)
        {
            return this.modules.GetAllowedHttpMethods(relativePath);
        }

        #endregion

        #region Misc

        private AuthenticationSchemes SelectScheme(HttpListenerRequest request)
        {
            string method = request.HttpMethod;
            string relativePath = PathAndMethod.GetRelativePath(request.Url);

            IHttpRequestModule requestModule = this.modules.GetRequestModules(relativePath).FirstOrDefault(m => m.Method == method);

            if (requestModule != null)
                return requestModule.CredentialType.ToSchemes();

            IHttpResponseModule responseModule = this.modules.GetResponseModules(relativePath).FirstOrDefault(m => m.Method == method);

            if (responseModule != null)
                return responseModule.CredentialType.ToSchemes();

            return AuthenticationSchemes.Anonymous;
        }

        #endregion

        #region Callback

        private void GetContextCallback(IAsyncResult r)
        {
            HttpListener lstnr = this.listener;
            HttpListenerContext context;
            HttpListenerRequest request;
            HttpListenerResponse response;

            #region Step 1: Get context, request and response.

            try
            {
                context = lstnr.EndGetContext(r);
                request = context.Request;
                response = context.Response;

                lstnr.BeginGetContext(this.GetContextCallback, null);
            }
            catch (Exception error)
            {
                base.OnError(new ExceptionsEventArgs(error));
                return;
            }

            #endregion

            #region Step 2: Find matched modules.

            string relativePath = PathAndMethod.GetRelativePath(request.Url);

            IEnumerable<IHttpRequestModule> requestModules = this.modules.GetRequestModules(relativePath);
            IEnumerable<IHttpResponseModule> responseModules = this.modules.GetResponseModules(relativePath);

            if (!requestModules.Any() && !responseModules.Any())
            {
                response.StatusCode = (int)HttpStatusCode.NotFound;
                response.Close();

                return;
            }

            #endregion

            #region Step 3: Check HTTP method.

            IHttpRequestModule requestModule = requestModules.FirstOrDefault(m => m.Method == request.HttpMethod);
            IHttpResponseModule responseModule = responseModules.FirstOrDefault(m => m.Method == request.HttpMethod);

            if (requestModule == null && responseModule == null)
            {
                response.Headers[HttpResponseHeader.Allow] = string.Join(", ", this.modules.GetAllowedHttpMethods(relativePath));
                response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
                response.Close();

                return;
            }

            #endregion

            #region Step 4: Validate Identity

            HttpListenerBasicIdentity identity = context.User != null ? context.User.Identity as HttpListenerBasicIdentity : null;
            string username = identity != null ? identity.Name : null;

            if (identity != null)
            {
                bool validated = false;

                try
                {
                    if (requestModule != null)
                        validated = requestModule.Validate(username, identity.Password);

                    if (responseModule != null)
                        validated = responseModule.Validate(username, identity.Password);
                }
#if DEBUG
                catch (Exception error)
                {
                    Debug.WriteLine(error);
#else
                catch
                {
#endif
                    response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    response.Close();

                    return;
                }

                if (!validated)
                {
                    response.StatusCode = (int)HttpStatusCode.Forbidden;
                    response.Close();

                    return;
                }
            }

            #endregion

            #region Step 5: Process request and response context.

            IEnumerable<Notification> notifications = null;

            try
            {
                notifications = requestModule != null ? requestModule.Read(new HttpRequestContext(username, request)) : null;
            }
#if DEBUG
            catch (Exception error)
            {
                Debug.WriteLine(error);
#else
            catch
            {
#endif
                response.StatusCode = (int)HttpStatusCode.InternalServerError;
                response.Close();

                return;
            }

            if (responseModule != null)
                ThreadPool.QueueUserWorkItem(this.RegisterContextCallback,
                    Tuple.Create(responseModule, new HttpResponseContext(username, request.RemoteEndPoint, request.Url, response)));
            else
            {
                response.StatusCode = (int)HttpStatusCode.OK;
                response.Close();
            }

            if (notifications != null)
                foreach (var notification in notifications)
                    this.OnNotify(new FireNotificationEventArgs(notification));

            #endregion
        }

        private void RegisterContextCallback(object state)
        {
            var tuple = state as Tuple<IHttpResponseModule, HttpResponseContext>;

            tuple.Item1.RegisterContext(tuple.Item2);
        }

        #endregion

        #region Raise Event

        /// <summary>
        /// Raises the <see cref="Notify"/> event.
        /// </summary>
        /// <param name="e">Event data for the event.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="e"/> is null.</exception>
        protected virtual void OnNotify(FireNotificationEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.Notify != null)
                this.Notify.BeginInvoke(this, e, this.Notify.EndInvoke, null);
        }

        #endregion
    }
}